/**
* Copyright 2014 55 Minutes (http://www.55minutes.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fiftyfive.wicket.css;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.list.Loop;
import org.apache.wicket.markup.html.list.LoopItem;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.RepeatingView;
/**
* Emits {@code odd}, {@code even}, {@code first} and {@code last} CSS classes (or any
* combination thereof) for repeating components. Supports {@link ListView}, {@link Loop} and
* subclasses of {@link RepeatingView}
* (e.g. {@link org.apache.wicket.markup.repeater.data.DataView DataView}).
* <p>
* To use, attach this behavior to the item being populated. Upon constructing the behavior
* you can specify exactly which classes you would like the behavior to manage. In this example,
* we ask for {@code first} and {@code last} to be emitted to each item in a list:
*
* <pre class="example">
* Example.html
*
* <ul>
* <li wicket:id="items"></li>
* </ul></pre>
*
* <pre class="example">
* Example.java
*
* add(new ListView("items", listOfThreeThings) {
* @Override
* protected void populateItem(ListItem it)
* {
* it.add(new IterationCssBehavior("first", "last"));
* }
* });</pre>
*
* <pre class="example">
* Output
*
* <ul>
* <li class="first"></li>
* <li></li>
* <li class="last"></li>
* </ul></pre>
*
* @since 2.0.4
*/
public class IterationCssBehavior extends CssClassModifier
{
/**
* Valid CSS classes that this behavior can be configured to emit during the rendering
* of repeating components.
*/
public enum CssClass
{
/** The "odd" css class, emitted on odd iterations. The first item is considered odd. */
ODD,
/** The "even" css class, emitted on even iterations. The first item is considered odd. */
EVEN,
/** The "first" css class, emitted on the first iteration only. */
FIRST,
/** The "last" css class, emitted on the last iteration only. */
LAST,
/**
* The "iteration" css class. It is emitted on every iteration, with a suffix added
* representing the iteration number. The numbering starts at 1. In other words:
* {@code iteration1}, {@code iteration2}, {@code iteration3}, etc.
*/
ITERATION
}
private final List<CssClass> classes = new ArrayList<CssClass>();
/**
* Construct a behavior that will output the specified css classes. The behavior must
* be added to a repeating item like an {@link Item} or {@link ListItem}.
* This is a convenience constructor that, while not typesafe, is much more concise.
* <pre class="example">
* // These are equivalent:
* new IterationCssBehavior("odd", "even");
* new IterationCssBehavior(IterationCssBehavior.CssClass.ODD, IterationCssBehavior.CssClass.EVEN);</pre>
*
* @param classes One or more of the 5 classes declared in the {@link CssClass} enum.
*
* @throws IllegalArgumentException if one or more of the specified strings does not
* exactly match a {@link CssClass} value (case-insensitive)
*/
public IterationCssBehavior(String... classes)
{
for(String cls : classes)
{
this.classes.add(CssClass.valueOf(cls.toUpperCase()));
}
}
/**
* Construct a behavior that will output the specified css classes. The behavior must
* be added to a repeating item like an {@link Item} or {@link ListItem}.
*
* @param classes One or more of the 5 classes declared in the {@link CssClass} enum.
*/
public IterationCssBehavior(CssClass... classes)
{
this.classes.addAll(Arrays.asList(classes));
}
/**
* For each of the {@link CssClass} values provided in the constructor, inspect the
* component to which this behavior is bound and determine if the css class is applicable.
* If so, add that class to the Set of classes that will be emitted in the markup.
* For example, if the class is "odd", determine if the component is odd-numbered within
* its repeating view; if so, add "odd" to the classes that will be emitted.
*/
@Override
protected void modifyClasses(Component component, Set<String> values)
{
final int iteration = getIndex(component) + 1;
final int size = getSize(component);
for(CssClass cls : this.classes)
{
switch(cls)
{
case ODD:
if(iteration % 2 == 1)
{
values.remove("even");
values.add("odd");
}
break;
case EVEN:
if(iteration % 2 == 0)
{
values.remove("odd");
values.add("even");
}
break;
case FIRST:
if(1 == iteration)
{
if(size > 1)
{
values.remove("last");
}
values.add("first");
}
break;
case LAST:
if(size == iteration)
{
if(size > 1)
{
values.remove("first");
}
values.add("last");
}
break;
case ITERATION:
values.add("iteration" + iteration);
break;
}
}
}
/**
* Assume that the component is a {@link ListItem},
* {@link LoopItem}, or {@link Item}
* and get its index. Note that the index begins from zero.
*
* @throws UnsupportedOperationException if the component is not one of the three supported
* types
*/
protected int getIndex(Component component)
{
if(component instanceof ListItem)
{
return ((ListItem) component).getIndex();
}
if(component instanceof LoopItem)
{
return ((LoopItem) component).getIndex();
}
if(component instanceof Item)
{
return ((Item) component).getIndex();
}
throw new UnsupportedOperationException(String.format(
"Don't know how to find the index of component %s (%s). " +
"Only list.ListItem, list.LoopItem and repeater.Item are supported. " +
"Perhaps you attached IterationCssBehavior to the wrong component?",
component.getPath(),
component.getClass()));
}
/**
* Assume that the component has an immediate parent of {@link ListView}, {@link Loop}, or
* {@link RepeatingView} and use what we know about those implementations to infer the
* size of the list that is being iterated. Note that in the case of pagination, this is the
* size of the visible items (i.e the current page), not the total size that includes other
* pages.
*
* @throws UnsupportedOperationException if the parent component is not one of the three
* supported types
*/
protected int getSize(Component component)
{
MarkupContainer parent = component.getParent();
if(parent instanceof ListView)
{
return ((ListView) parent).getViewSize();
}
if(parent instanceof Loop)
{
return ((Loop) parent).getIterations();
}
if(parent instanceof RepeatingView)
{
// TODO: more efficent way?
int size = 0;
Iterator iter = parent.iterator();
while(iter.hasNext())
{
iter.next();
size ++;
}
return size;
}
throw new IllegalStateException(String.format(
"Don't know how to find the size of the repeater that contains component " +
"%s (%s). " +
"Only list.ListItem, list.LoopItem and repeater.Item are supported. " +
"Perhaps you attached IterationCssBehavior to the wrong component?",
component.getPath(),
component.getClass()));
}
}